1

我们知道 React 的标准模式是单向数据流,而其表单项通常需要监听 onChange 事件,然后通过改变外部的 value 来回写表单项的 value,譬如如下 input

class App extends React.Component {
  constructor( props ) {
    super( props );
    this.state = {
      inputValue: 'default'
    }

    this.inputChangeHandler = ( e )=>{
      this.setState( {
        inputValue: e.target.value
      } );
    }
  }
  render() {
    return (
      <div>
        <form>
          <input
            value={ this.state.inputValue }
            onChange={ this.inputChangeHandler }
          />
        </form>
      </div>
    )
  }
}

如果表单有很多表单项,那么这种标准的做法需要你写很多个 state 的属性和很多个 onChange 监听函数,这是一个体力活儿。但是一般的表单应用其实不需要实时监控表单项的用户输入,用 defaultValue 足以,在表单项目 onBlur 或者最后提交的时候一次验证获取用户输入即可,譬如:

class App extends React.Component {
  constructor( props ) {
    super( props );

    this.submit = ( e )=>{
      let userInputValue = this.refs.userInput.value;
      // 1. 验证 userInputValue
      // 2. 提交表单
    }
  }
  render() {
    return (
      <div>
        <form>
          <input 
            ref="userInput"
            defaultValue="default"
          />
          <button onClick={ this.submit }>提交</button>
        </form>
      </div>
    )
  }
}

这样就可以少写不少代码,当然你可以写一些工具去批量添加所有的 onChange 事件监听函数和对应的 state 的属性,譬如 redux-form。(回头一想,这种写法在提交时候也需要写很多获取用户输入的代码,如果使用第一种正模式,那么提交时候只需要获取 state 就可以了,不过这里先不讨论这些)<br/>
对于一个表单而言,通常还需要重置功能(reset),如果是第一种正模式的写法,我们只要保存一份初始化的默认值,在用户点击到了重置后,通过 setState 设回去就行了。但是如果使用第二种 defaultValue 的写法,那么就没有办法了,因为 defaultValue 只在第一次创建虚拟 dom 的时候有作用,如果 dom 不改变你改变 defaultValue 是没有用的。这个时候该怎么办呢?<br/>
嘿嘿!这个时候我们就可以用到这个奇技淫巧了。既然 defaultValue 是在创建虚拟 dom 的时候有用,那么我们在用户点击重置的时候让 React 重新创建这些表单项的虚拟 dom 不就好了么。根据 React 虚拟 dom diff 的算法,只要改变 dom 节点的类型就能促使在 diff 的时候重新创建虚拟 dom。具体的写法我们就用代码来演示下:

class App extends React.Component {
  constructor( props ) {
    super( props );
    // fieldSetWrapperType 是一个标志位属性,render 中会根据这个变量的值的不同,渲染不同的元素
    this.fieldSetWrapperType = 'div';
    this.submit = ( e )=>{
      let userInputValue = this.refs.userInput.value;
      // 1. 验证 userInputValue
      // 2. 提交表单
    }
    this.reset = ()=>{
      // 点击重置,改变标志位
      this.fieldSetWrapperType = this.fieldSetWrapperType === 'div' ? 'section' : 'div';
      // 强制刷新这个组件
      this.forceUpdate();
    }
  }
  // 把表单项的渲染抽象到一个方法中,避免重复编码
  renderFieldSet() {
    return (
      <input 
        ref="userInput"
        defaultValue="default"
      />
    );
  }
  render() {
    return (
      <div>
        <form>
          {
          /* 根据 fieldSetWrapperType 值的不同,渲染不同的元素(表单项的 wrapper 元素) */
          this.fieldSetWrapperType === 'div' ? 
          <div className="wrapper">{ this.renderFieldSet() }</div>
          :
          <section className="wrapper">{ this.renderFieldSet() }</section>
          }
          <button onClick={ this.submit }>提交</button>
          <button onClick={ this.reset }>重置</button>
        </form>
      </div>
    )
  }
}

思路就是在表单项外面包一层元素,每次点击重置后改变一个变量,再强制刷新这个组件,组件根据这个变量不同的值把这个包装元素的 type 改变,那么它下面的所有表单项的虚拟 dom 都会被重新创建,达到了重置的目的。不过这个效果依赖于 React 虚拟dom diff 算法。如果以后算法改变了,那么可能就失效了,而且这个写法是反模式的,我初衷是想在处理巨型表单时候少写点代码偷懒用。如果使用时候出现什么副作用,鄙人概不负责。

此技巧在写文章时 React 正处于 15.4.x 的版本


Bernie维尼
388 声望21 粉丝